package org.reborn.math

/**
 * Class for representing matrices composed of Floats
 * 
 * @param mat The matrix as a 2 Dimensional list
 */
case class MatrixF(val columns:Int, val rows:Int) {
  private val mat:Array[Float] = new Array(columns * rows)
  
  def apply(column:Int, row:Int):Float = { mat(row * columns + column) }
  def update(column:Int, row:Int, value:Float){ mat(row * columns + column) = value }
  
  /**
   * @param row the specified row. 0 is the first row
   * @return the row specified by row as a VectorF
   */
  def row(row:Int):VectorF = { 
	val vec = new VectorF(columns)
	for(i <- 0 until columns){
	  vec(i) = apply(i,row)
	}
	vec
  }
  
  /**
   * Update the specified row of a matrix with values in the Vector
   * @param row the specified row, 0 is the first row
   * @param vec the vector to update the row's values from
   */
  def updateRow(row:Int,vec:VectorF){
	for(i <- 0 until vec.length){
	  update(i,row,vec(i))
	}
  }
  
  /**
   * @param column the specified column. 0 is the first column
   * @return the column specified by column as a VectorF
   */
  def column(column:Int):VectorF = { 
	val vec = new VectorF(rows)
	for(i <- 0 until rows){
	  vec(i) = apply(column,i)
	}
	vec
  }
  
  /**
   * Update the specified column of a matrix with values in the Vector
   * @param column the specified column, 0 is the first column
   * @param vec the vector to update the column's values from
   */
  def updateColumn(column:Int,vec:VectorF){
	for(i <- 0 until vec.length){
	  update(column,i,vec(i))
	}
  }
  
  /**
   * Multiply 2 matrices together. The number of columns in the first matrix (width)
   * must equal the number of rows in the second matrix (height).
   * @param o the MatrixF to multiply with
   * @return a new MatrixF with the result of the multiplication
   */
  def *(o:MatrixF):MatrixF = {
	require(this.columns == o.rows, "Columns in first matrix ("+this.columns+") " +
			"does not equal rows of second matrix ("+o.rows+")")
	
	val newMat = new MatrixF(o.columns,this.rows)
	
	for(row <- 0 until this.rows ;
	    col <- 0 until o.columns){
	  newMat(col,row) = this.row(row) dot o.column(col)
	}
	newMat
  }

  /**
   * Multiplies the current matrix by a VectorF. Returns the transformed 
   * VectorF. If the length of the vector is the same as the # of columns of the matrix,
   * then the multiplication is done normally. If the length of the vector is less
   * than the # of columns of the matrix, then additional floats (1.0f) will be padded to the
   * end of the vector before the multiplication is done. Multiplication with a vector
   * that is longer than the # of columns of the matrix is undefined.
   * 
   * The transformed VectorF's length will equals the number of columns 
   * of the matrix, regardless of the length of the vector that is passed in. 
   * @param vec The vector to transform by this matrix
   * @return The transformed vector. Its length will equals the matrix's columns.
   */
  def *(vec:VectorF):VectorF = {
	val vecMat = new MatrixF(1,rows)
	
	var i = vec.length
	while(i < rows){
	  vecMat(0,i) = 1.0f
	  i+=1
	}
		
	vecMat.updateColumn(0,vec)
	
	val finalMat = this * vecMat
	
	finalMat.column(0)
  }
  
  /**
   * Returns a new matrix that is the transpose of this matrix 
   * @param vec The vector to transform by this matrix
   * @return The transformed vector. Its length will equals the matrix's columns.
   */
  def transpose:MatrixF = {
	val newMat = new MatrixF(rows,columns)
	
	for(col <- 0 until columns;
	    row <- 0 until rows){
	  newMat(row,col) = apply(col,row)
	}
		
	newMat
  }
  
  /**
   * Returns a copy of the matrix data as a 1-dimensional array.
   * Creates a copy of the array each time the method is called,
   * to prevent mutability outside of the MatrixF object. 
   * @return a copy of the matrix data as a 1-dimensional array
   */
  def as1DArray:Array[Float] = mat.clone
  
  /**
   * Returns a copy of the matrix data as a 1-dimensional array
   * in the format that OpenGL expects.
   * Creates a copy of the array each time the method is called,
   * to prevent mutability outside the MatrixF object. 
   * The same functionality can be accomplished by calling
   * transpose.as1DArray
   * @return a copy of the matrix data as a 1-dimensional array
   * in the format expected by OpenGL
   */
  def asOpenGLMatrix:Array[Float] = {
	transpose.as1DArray
  }
}

object MatrixF{
	
  /**
   * Creates an Identity matrix of the specified dimension
   * @param size the square dimension of the matrix
   * @return the Identity Matrix
   */
  def IdentityMat(size:Int):MatrixF = {
    val newMat = new MatrixF(size,size)
    for(i <- 0 until size){
      newMat(i,i) = 1.0f
    }
    newMat
  }
  
  /**
   * Creates an Identity matrix of dimension 4x4
   * @return the Identity Matrix
   */
  def IdentityMat():MatrixF = {
    IdentityMat(4)
  }
  
  /**
   * Creates a 4x4 rotation matrix around the X axis
   * @param radians the angle in radians to rotate
   * @return the rotation matrix
   */
  def RotateXMat(radians:Float):MatrixF = {
	val (c,s) = trigAngles(radians)
	  
	val newMat = IdentityMat(4)
	newMat(1,1) = c
	newMat(2,1) = -s
	newMat(1,2) = s
	newMat(2,2) = c
	newMat
  }
  
  /**
   * Creates a 4x4 rotation matrix around the Y axis
   * @param radians the angle in radians to rotate
   * @return the rotation matrix
   */
  def RotateYMat(radians:Float):MatrixF = {
	val (c,s) = trigAngles(radians)
	  
	val newMat = IdentityMat(4)
	newMat(0,0) = c
	newMat(2,0) = s
	newMat(0,2) = -s
	newMat(2,2) = c
	newMat
  }
  
  /**
   * Creates a 4x4 rotation matrix around the Z axis
   * @param radians the angle in radians to rotate
   * @return the rotation matrix
   */
  def RotateZMat(radians:Float):MatrixF = {
	val (c,s) = trigAngles(radians)
	  
	val newMat = IdentityMat(4)
	newMat(0,0) = c
	newMat(1,0) = -s
	newMat(0,1) = s
	newMat(1,1) = c
	newMat
  }
  
  /**
   * Creates a 4x4 translation matrix
   * @param x amount to translate in the X direction
   * @param y amount to translate in the Y direction
   * @param z amount to translate in the Z direction
   * @return the translation matrix
   */
  def TranslateMat(x:Float,y:Float,z:Float):MatrixF = {
	val newMat = IdentityMat(4)
	newMat(3,0) = x
	newMat(3,1) = y
	newMat(3,2) = z
	newMat
  }
  
  /**
   * Creates a 4x4 translation matrix
   * @param scaleX amount to scale in the X direction
   * @param scaleY amount to scale in the Y direction
   * @param scaleZ amount to scale in the Z direction
   * @return
   */
  def ScaleMat(scaleX:Float,scaleY:Float,scaleZ:Float):MatrixF = {
	val newMat = IdentityMat(4)
	newMat(0,0) = scaleX
	newMat(1,1) = scaleY
	newMat(2,2) = scaleZ
	newMat
  }
  
  private def trigAngles(radians:Float):(Float,Float) = {
	(math.cos(radians).asInstanceOf[Float],
	 math.sin(radians).asInstanceOf[Float])  
  }
}